/**
 * Team members:
 * HengXing Liu 2022229044
 * YiYang Zhang 2022229047
 * ZeJun Liu 2022229092
 */
package project;

import java.io.BufferedReader;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * This class implements a tokenizer. A tokenizer is used to
 * split a string (user input) in tokens
 * YOU MUST COMPLETE AND USE THIS CLASS WITHOUT CHANGING
 * THE PUBLIC METHODS
 */
public class Tokenizer {

	private String data;
	private int cur;
	private Token nextToken;
	private final String operators = "+-*/()=^";
	private final String identifiers = "setprecision,let,reset,last,save,load,saved,log,end,logged,define,sin,cos,tan,abs";
	private final Set<String> setOfIdt = new HashSet<>(Arrays.asList(identifiers.split(",")));
	/**
	 * create a tokenizer on the string 'input'
     * YOU MAY ADD THROW CLAUSES TO THIS CONSTRUCTOR
	 */
	public Tokenizer(String input) throws LexicalErrorException, TokenException {
		data = input;
		cur = 0;
		loadNextToken();
	}

	/**
	 * checks if there is more token to read
	 */
	public boolean hasNextToken() {
		if(cur >= data.length() && nextToken == null){
			return false;
		}
		return true;
	}

	/**
	 * returns the next token to be read
	 * Throws a TokenException if there is
	 * no more token to peek in the Tokenizer
	 * YOU MAY ADD THROW CLAUSES TO THIS METHOD
	 */
	public Token peekNextToken() throws TokenException {
		if(!hasNextToken()){
			throw new TokenException("CANNOT PEEK: There is no more token to peek");
		}
		return nextToken;
	}

	/**
	 * reads and returns the next token to be read
	 * (i.e. the next token is removed from the tokenizer)
     * YOU MAY ADD THROW CLAUSES TO THIS METHOD
	 */
	public Token readNextToken() throws TokenException, LexicalErrorException {
		if(!hasNextToken()){
			throw new TokenException("CANNOT READ: There is no more token to read");
		}
		Token ret = copyToken(nextToken);
		loadNextToken();
		return ret;
	}

	
	////// add your private methods below
	private Token copyToken(Token token) throws TokenException {
		Token ret = null;
		if(token.isDelimiter()){
			ret = Token.makeCOMMA();
		}else if(token.isEqual()){
			ret = Token.makeEQUAL();
		}else if(token.isNumber()){
			ret = Token.makeNUMBER(token.getNumber());
		}else if(token.isIdentifier()){
			ret = Token.makeIDENTIFIER(token.getIdentifier());
		}else if(token.isString()){
			ret = Token.makeSTRING(token.getString());
		}else if(token.isOperator()){
			switch (token.getOperator().charAt(0)) {
				case '=' : ret = Token.makeEQUAL();break;
				case '(' : ret = Token.makeOPENPAR();break;
				case ')' : ret = Token.makeCLOSEPAR();break;
				case '+' : ret = Token.makePLUS();break;
				case '*' : ret = Token.makeTIMES();break;
				case '/' : ret = Token.makeDIVIDE();break;
				case '^' : ret = Token.makePOWER();break;
				case '-' :
					if(token.getOperator().length() == 2){
						ret = Token.makeUNARYMINUS();
					}else{
						ret = Token.makeMINUS();
					}
					break;
			};
		}
		return ret;
	}
	private void loadNextToken() throws LexicalErrorException, TokenException {
		if(cur == data.length()){
			nextToken = null;
			return ;
		}
		while(cur < data.length() && data.charAt(cur) == ' ') cur++;
		char nowChar = data.charAt(cur);
		String nowStr = String.valueOf(nowChar);
		if(operators.contains(nowStr)){
			makeOprToken(cur, nowChar);
			cur++;
		}else if(Character.isDigit(nowChar)){
			makeNumToken();
		}else if(nowChar == ','){
			nextToken = Token.makeCOMMA();
			cur++;
		}else{
			try{
				makeStrKindToken();
			}catch(LexicalErrorException e){
				throw e;
			}

		}
	}

	private void makeOprToken(int cur, char nowChar) throws TokenException {
		switch (nowChar) {
			case '=' : nextToken = Token.makeEQUAL();break;
			case '(' : nextToken = Token.makeOPENPAR();break;
			case ')' : nextToken = Token.makeCLOSEPAR();break;
			case '+' : nextToken = Token.makePLUS();break;
			case '*' : nextToken = Token.makeTIMES();break;
			case '/' : nextToken = Token.makeDIVIDE();break;
			case '^' : nextToken = Token.makePOWER();break;
			case '-' : {
				// if there is pre token is not a number and not a ')', the '-' will be UNARYMINUS
				if(nextToken == null || (!nextToken.isNumber() && !nextToken.isIdentifier() &&
						(!nextToken.isOperator() || (nextToken.isOperator() && !nextToken.getOperator().equals(")"))))){
					nextToken = Token.makeUNARYMINUS();
				} else {
					nextToken = Token.makeMINUS();
				}
			}break;
		}
	}

	private void makeNumToken(){
		String tmp = "";
		while(cur < data.length() &&(Character.isDigit(data.charAt(cur)) || data.charAt(cur) == '.')){
			tmp += data.charAt(cur);
			cur++;
		}
		nextToken = Token.makeNUMBER(Double.valueOf(tmp));
	}

	private void makeStrKindToken() throws LexicalErrorException {
		if(data.charAt(cur) == '"'){
			makeStringToken();
		}else{
			String tmp = "";
			char nowChar = data.charAt(cur);
			while(cur < data.length() &&
					(!operators.contains(String.valueOf(nowChar)) && nowChar != ' '&& nowChar != ',')){
				tmp += data.charAt(cur);
				cur++;
				if(cur < data.length()) nowChar = data.charAt(cur);
			}
			if(setOfIdt.contains(tmp)){
				nextToken = Token.makeIDENTIFIER(tmp);
			}else{
				if(checkValidVariable(tmp)){
					nextToken = Token.makeIDENTIFIER(tmp);
				}else{
					nextToken = Token.makeSTRING(tmp);
					throw new LexicalErrorException(tmp + " is not a valid character");
				}
			}
		}
	}

	private boolean checkValidVariable(String tmp){
		if(!Character.isAlphabetic(tmp.charAt(0))){
			return false;
		}
		for(int i = 0; i < tmp.length(); i++){
			char nowChar = tmp.charAt(i);
			if(!(Character.isDigit(nowChar) || Character.isAlphabetic(nowChar) || nowChar == '_')){
				return false;
			}
		}
		return true;
	}

	private void makeStringToken(){
		String tmp = "";
		tmp += "\"";
		cur++;
		while(data.charAt(cur) != '"'){
			tmp += data.charAt(cur);
			cur++;
		}
		tmp += "\"";
		cur++;
		nextToken = Token.makeSTRING(tmp);
	}

}